package com.swak.fx.support.ui;

import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import org.w3c.dom.Document;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLDocument;
import org.w3c.dom.html.HTMLElement;

import com.google.common.io.Resources;
import com.sun.javafx.webkit.Accessor;
import com.sun.webkit.WebPage;
import com.swak.utils.Maps;
import com.swak.utils.StringUtils;

import javafx.concurrent.Worker.State;
import javafx.geometry.Pos;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import netscape.javascript.JSObject;

/**
 * 编辑器
 * 
 * @author DELL
 */
public class UIEditor extends StackPane implements UILifecycle {

	private final String STYLE_CLASS = "ui-editor";
	private final String STYLESHEET = UI.load("css/UIEditor.css");
	private final URL HTML = UI.loadURL("html/UIEditor.html");

	@Getter
	private final WebView webView;
	@Getter
	private final WebPage webPage;
	@Getter
	private final WebEngine webEngine;
	@Getter
	private final Keys keys = new Keys();

	public UIEditor() {
		this.webView = new WebView();
		this.webEngine = webView.getEngine();
		this.webPage = Accessor.getPageFor(webView.getEngine());
		this.init();
	}

	@Override
	public void initializeParts() {
		getStyleClass().add(STYLE_CLASS);
		this.webView.setFocusTraversable(true);
		this.webView.setContextMenuEnabled(false);
		this.webPage.setEditable(false);
		this.loadHtml(this.loadEditorHtml());
		this.getChildren().add(webView);
		this.setAlignment(Pos.CENTER);
	}

	private String loadEditorHtml() {
		try {
			List<String> lines = Resources.readLines(HTML, StandardCharsets.UTF_8);
			return StringUtils.join(lines, "\n");
		} catch (Exception e) {
			throw new IllegalArgumentException(e);
		}
	}

	@Override
	public void setupEventHandlers() {
		this.webView.addEventFilter(KeyEvent.KEY_TYPED, e -> {
			int code = e.getCharacter().hashCode();
			if (code == 13) {
				e.consume();
				this.keys.releaseAll();
			}
		});
		this.webView.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
			if (e.getCode() == KeyCode.V && e.isControlDown()) {
				e.consume();
				this.keys.releaseAll().keyPressed(KeyCode.CONTROL).keyPressed(KeyCode.V);
				this.$emit(Event.Copy);
			}
		});
		this.webView.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
			if (e.getCode() == KeyCode.V && e.isControlDown()) {
				e.consume();
				this.keys.releaseAll();
			}
		});
		this.webView.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
			if (e.getCode() == KeyCode.UNDEFINED || e.getCode() == KeyCode.BACK_SPACE
					|| e.getCode() == KeyCode.DELETE) {
				e.consume();
				return;
			}
			this.keys.keyPressed(e.getCode());
			this.$emit(Event.Key_Typed, keys);
		});
		this.webView.addEventHandler(KeyEvent.KEY_RELEASED, e -> {
			if (e.getCode() == KeyCode.UNDEFINED || e.getCode() == KeyCode.BACK_SPACE
					|| e.getCode() == KeyCode.DELETE) {
				e.consume();
				return;
			}
			this.keys.keyReleased(e.getCode());
		});
		this.webView.setOnDragOver((e) -> {
			e.acceptTransferModes(TransferMode.COPY);
			this.webView.requestFocus();
		});
		this.webView.setOnDragDropped((e) -> {
			TransferMode mode = e.getAcceptedTransferMode();
			if (mode == TransferMode.COPY) {
				e.setDropCompleted(true);
				this.$emit(Event.Drag_Copy, e.getDragboard());
			}
		});
	}

	@Override
	public void setupValueChangedListeners() {
		this.focusedProperty().addListener((ob, o, n) -> {
			if (n) {
				this.webView.requestFocus();
			}
		});
	}

	@Override
	public void initializeFinish() {
		if (this.webEngine.getLoadWorker().stateProperty().get() == State.SUCCEEDED) {
			this.onWebEngineLoadSuccess();
		} else {
			this.webEngine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
				if (newValue == State.SUCCEEDED) {
					this.onWebEngineLoadSuccess();
				}
			});
		}
	}

	// ================================================================================
	// Listeners
	// ================================================================================
	private final $App app = new $App();

	private void onWebEngineLoadSuccess() {

		// register app
		JSObject win = (JSObject) this.webEngine.executeScript("window");
		win.setMember("$App", app);

		// monitor event
		Document doc = webEngine.getDocument();
		((EventTarget) doc).addEventListener("dblclick", (org.w3c.dom.events.Event evt) -> {
			evt.stopPropagation();
			EventTarget target = evt.getTarget();
			HTMLElement element = null;
			if (target == null || (element = target instanceof HTMLElement ? (HTMLElement) target : null) == null
					|| element.getNodeName().equals("BODY") || element.getNodeName().equals("HTML")) {
				return;
			}
			this.$emit(Event.Dblclick, element);
		}, true);
	}

	class $App {
	}

	// ================================================================================
	// Event
	// ================================================================================
	private Map<Event, Consumer<Object>> handlers = Maps.newHashMap();

	public void $emit(Event event) {
		this.$emit(event, null);
	}

	public void $emit(Event event, Object param) {
		Consumer<Object> handler = handlers.get(event);
		if (handler != null) {
			handler.accept(param);
		}
	}

	public void $on(Event event, Consumer<Object> handler) {
		handlers.put(event, handler);
	}

	public static enum Event {

		/**
		 * 复制
		 */
		Copy,

		/**
		 * 拖动复制
		 */
		Drag_Copy,

		/**
		 * 键盘事件
		 */
		Key_Typed,

		/**
		 * 双击事件
		 */
		Dblclick,
	}

	// ================================================================================
	// Function
	// ================================================================================

	/**
	 * 重新加载 Html 内容， 
	 *  
	 * @param html
	 */
	public final void loadHtml(String html) {
		this.webPage.load(webPage.getMainFrame(), html, "text/html");
	}

	/**
	 * 插入新行
	 */
	public final void newLine() {
		this.webPage.executeCommand("insertnewline", null);
	}

	/**
	 * 插入到光标处
	 *  
	 * @param html
	 */
	public final void insertHtml(String html) {
		try {
			this.webEngine.executeScript(StringUtils.format("insertHtmlAtCursor('%s')", html));
		} catch (Exception e) {
			this.appendHtml(html);
		}
	}

	/**
	 * 追加 body
	 *  
	 * @param html
	 */
	public final void appendHtml(String html) {
		try {
			this.webEngine.executeScript(StringUtils.format("appendHtml('%s')", html));
		} catch (Exception e) {
		}
	}

	/**
	 * 清空 body
	 *  
	 * @param html
	 */
	public final void clearBodyHtml() {
		try {
			this.webEngine.executeScript(StringUtils.format("initHTML('%s')", StringUtils.EMPTY));
		} catch (Exception e) {
		}
	}

	/**
	 * 设置 Html 到 body 中, 会删除之前的内容
	 *  
	 * @param html
	 */
	public final void setBodyHtml(String html) {
		try {
			this.webEngine.executeScript(StringUtils.format("initHTML('%s')", html));
		} catch (Exception e) {
		}
	}

	/**
	 * 获得输入节点的内容
	 * @return
	 */
	public final String getBodyHTML() {
		return String.valueOf(this.webEngine.executeScript("getBodyHTML()"));
	}

	/**
	 * 获得所有的 Html 信息
	 * 
	 * @return
	 */
	public final String getHtml() {
		return this.webPage.getHtml(this.webPage.getMainFrame());
	}

	/**
	 * 获取 Html 中 Document
	 * 
	 * @return
	 */
	public final HTMLDocument getHTMLDocument() {
		HTMLDocument doc = (HTMLDocument) webPage.getDocument(webPage.getMainFrame());
		HTMLDocument htmlDocument = (HTMLDocument) doc;
		return htmlDocument;
	}

	/**
	 * 获取 Html 中 Body
	 * 
	 * @return
	 */
	public final HTMLElement getHTMLBody() {
		HTMLDocument doc = (HTMLDocument) webPage.getDocument(webPage.getMainFrame());
		HTMLDocument htmlDocument = (HTMLDocument) doc;
		HTMLElement htmlDocumentElement = (HTMLElement) htmlDocument.getDocumentElement();
		HTMLElement htmlBodyElement = (HTMLElement) htmlDocumentElement.getElementsByTagName("body").item(0);
		return htmlBodyElement;
	}

	// ================================================================================
	// Overridden Methods
	// ================================================================================

	@Override
	public String getUserAgentStylesheet() {
		return STYLESHEET;
	}

	/**
	 * 按键组合
	 * 
	 * @author DELL
	 */
	@Getter
	@Setter
	@Accessors(chain = true)
	@ToString
	public static class Keys {

		private KeyCode key1 = KeyCode.UNDEFINED;
		private KeyCode key2 = KeyCode.UNDEFINED;
		private KeyCode key3 = KeyCode.UNDEFINED;

		public Keys releaseAll() {
			key1 = KeyCode.UNDEFINED;
			key2 = KeyCode.UNDEFINED;
			key3 = KeyCode.UNDEFINED;
			return this;
		}

		public Keys keyPressed(KeyCode code) {
			if (key1 == code || key2 == code || key3 == code) {
				return this;
			}
			if (key1 == KeyCode.UNDEFINED) {
				key1 = code;
			} else if (key2 == KeyCode.UNDEFINED) {
				key2 = code;
			} else if (key3 == KeyCode.UNDEFINED) {
				key3 = code;
			}
			return this;
		}

		public Keys keyReleased(KeyCode code) {
			if (key1 == code) {
				key1 = KeyCode.UNDEFINED;
			} else if (key2 == code) {
				key2 = KeyCode.UNDEFINED;
			} else if (key3 == code) {
				key3 = KeyCode.UNDEFINED;
			}
			return this;
		}

		/**
		 * 不需要保证顺序
		 */
		@Override
		public int hashCode() {
			return key1.hashCode() + key2.hashCode() + key3.hashCode();
		}

		/**
		 *  考虑顺序的不一样
		 */
		@Override
		public boolean equals(Object obj) {
			if (obj == null || !(obj instanceof Keys)) {
				return false;
			}
			Keys other = (Keys) obj;

			if (key1 == other.key1 && key2 == other.key2 && key3 == other.key3) {
				return true;
			}
			if (key1 == other.key1 && key2 == other.key3 && key3 == other.key2) {
				return true;
			}

			if (key1 == other.key2 && key2 == other.key1 && key3 == other.key3) {
				return true;
			}
			if (key1 == other.key2 && key2 == other.key3 && key3 == other.key1) {
				return true;
			}

			if (key1 == other.key3 && key2 == other.key1 && key3 == other.key2) {
				return true;
			}
			if (key1 == other.key3 && key2 == other.key2 && key3 == other.key1) {
				return true;
			}
			return false;
		}
	}
}
